# -*- coding: utf-8 -*-
import serial
import time
import logging
from data_model import *

from PyQt4.QtCore import *
from PyQt4.QtGui import *

class SerialComm:
	def __init__(self, device):
		self.device = device

	def device_write(self, request):
		logging.info("%d > %r", self.device.port + 1, request)
		self.device.write(request)

	def read_answer(self):
		response = b''
		while True:
			char = self.device.read()
			logging.info("%d < %r", self.device.port + 1, char)
			if char == b'' or char == b'\r':
				return response
			response += char
	def do_command(self, request, response):
		logging.info("Sending %r, expecting answer %r", request, response)
		self.device_write(request)
		answer = self.read_answer()
		if answer != response:
			raise RuntimeError("PORT {} Wrong response to request {!r} expected {!r} but was {!r}".format(self.device.port, request, response, answer))

class Discharger(SerialComm):
	def __enter__(self):
		self.do_command(b'#R\r', b'R')
		logging.info("Safety wait")
		QThread.msleep(1000)
		logging.info("Safety wait done")
	def __exit__(self, exc_type, exc_value, traceback):
		self.do_command(b'#E\r', b'E')
	def measure(self):
		self.do_command(b'#m\r', b'm')
		data = self.device.read(4)
		voltage = ((data[0] << 8) + data[1]) / 50
		current = -((data[2] << 8) + data[3])
		return (voltage, current)

class Charger(SerialComm):
	def __enter__(self):
		self.device_write(b'#S\r')
		while True:
			resp = self.read_answer()
			if resp == b'S':
				return
			elif resp.startswith(b'M'):
				if len(resp) != 13:
					logging.error("Malformed measure data {!r}".format(resp))
					raise RuntimeError("Malformed measure data {!r}".format(resp))
					# skip measure data
			else:
				logging.error("Unknown response {}".format(resp))
				raise RuntimeError("Unknown response {}".format(resp))
		logging.info("Safety wait")
		QThread.msleep(1000)
		logging.info("Safety wait done")
	def __exit__(self, exc_type, exc_value, traceback):
		self.device_write(b'#X\r')
		while True:
			resp = self.read_answer()
			if resp == b'X':
				return
			# discarding measure data (maybe superfluous
			elif resp.startswith(b'M'):
				if len(resp) != 13: raise RuntimeError("Malformed measure data")
				# skip measure data
			else:
				raise RuntimeError("Unknown response {}".format(resp))
	
	def explicit_measure(self):
		self.ask_measure()
		return self.measure()

	def ask_measure(self):
		self.device_write(b'#M\r')
	
	def measure(self):
		''' waits and expects measure data to come from the device, and return it as a tuple '''
		old_timeout = self.device.timeout
		self.device.timeout = 20
		logging.info("Waiting for measure data...")
		resp = self.read_answer()
		if not resp.startswith(b'M'):
			logging.error("Measure not echoed, got %r instead", resp)
			raise RuntimeError("Measure not echoed, got {!r} instead".format(resp))
		resp = resp[1:]
		self.device.timeout = old_timeout

		try:
			voltage = int(resp[0:4], 16) / 100
			voltage2 = int(resp[4:8], 16) / 100
			current = int(resp[8:11], 16) / 10
			temp = int(resp[11:14], 16) / 10
			status = int(resp[15:16], 16)
		except ValueError:
			logging.error("Malformed measure")
			raise RuntimeError("Malformed measure")

		if status & 8:
			logging.info("Charging error")
		return (voltage, voltage2, current, temp, status)

def scan_serial():
	"""scan for available ports. return a list of tuples (num, name)"""
	available = []
	for i in range(256):
		try:
			s = serial.Serial(i)
			available.append((i, s.portstr))
			s.close()   # explicit close 'cause of delayed GC in java
		except serial.SerialException:
			pass
	return available

		
class Cycles(QThread):
	def tr(self, text):
		''' dummy fix for debugger '''
		return text
	def __init__(self, num_cycles, settings, on_changed_signal, 
		measure_arrived, cycle_started,
		cycles_model, measure_model, use_mock=False, parent=None):
		# FIXME cycle_settings could not be none
		QThread.__init__(self, parent)
		self.num_cycles = num_cycles
		self.settings = settings
		self.status_signal = on_changed_signal
		self.measure_arrived = measure_arrived
		self.cycle_started = cycle_started
		self.cycles_model = cycles_model
		self.measure_model = measure_model
		self.use_mock=use_mock
		self.charger = self.discharger = None
		self.init_devices()
	
	def __del__(self):
		logging.debug("Destroying device object")
		charger_device = self.charger.device
		if self.charger.device != self.discharger.device:
			del self.discharger.device
		del self.charger.device
		
	def init_devices(self):
		if self.use_mock:
			self.charger = MockCharger()
			self.discharger = MockDischarger()
		else:
			if not self.charger:
				# init charger
				port = int(self.settings['charge/device'])
				baudrate = int(self.settings['charge/baud'])
				device = serial.Serial(port=port, baudrate=baudrate, timeout=2)
				self.charger = Charger(device)

			if not self.discharger:
				# init discharger
				self.discharger = Discharger(device)
		
	def run(self):
		try:
			for i in range(self.num_cycles):
				self.current_cycle = i + 1
				self.cycle_start = time.time()
				self.discharge()
				self.charge()
		except serial.SerialException as e:
			logging.error("whoops, serial comm error", exc_info=e)
			raise
			#self.exit(-1)
		finally:
			self.quit()

	_skip_requested = False
	@property
	def skip_requested(self):
		res = self._skip_requested
		do_skip = res == self.stage
		if res:
			self._skip_requested = False
			if do_skip:
				self.status_signal.emit(0)
		return do_skip

	@pyqtSlot(int)
	def on_skipped(self, i):
		""" Called when user wants to cancel waiting """
		logging.info("Skip requested")
		self._skip_requested = i

	def wait_for(self, seconds):
		logging.info("waiting for %d seconds", seconds)
		for i in range(seconds * 5):
			self.status_signal[int, int, int].emit(0, i, seconds * 5)
			QThread.msleep(200)
			if i % 50 == 0:
				voltage, voltage2, current, temp, status = self.charger.explicit_measure()
				self.store_measure(voltage, voltage2, current, temp)
			if self.skip_requested: return True
		self.status_signal.emit(0)
		return False

	def store_measure(self, voltage, voltage2, current, temperature=None):
		logging.info("Got U = %s V; Got U2 = %r V; I = %r A; t = %r", voltage, voltage2, current, temperature)

		measure = self.measure_model.record()
		measure.setValue("cycle_id", self.cycle_id)
		measure.setValue("current", current)
		measure.setValue("voltage", voltage)
		measure.setValue("voltage2", voltage2)
		measure.setValue("temperature", temperature)
		measure.setValue("ts", time.time() - self.cycle_start)
		if not self.measure_model.insertRecord(0, measure): raise RuntimeError("Could not insert measure data")
		self.measure_arrived.emit()
		
	def create_new_cycle(self):
		cycle = self.cycles_model.record()
		for key in CYCLE_SETTINGS_SPINBOX:
			cycle.setValue(key, self.settings[key.replace('_', '/', 1)])
		self.cycles_model.insertRecord(-1, cycle)
		row_count = rows_in_table('cycles', self.cycles_model.database())
		self.cycle_id = self.cycles_model.record(row_count - 1).value("id")
		self.cycle_started.emit(self.current_cycle, self.num_cycles)
		
	def change_stage(self, stage):
		logging.info("Current stage is %s", stage)
		self.stage = stage
		self.status_signal.emit(stage)
		
	def discharge(self):
		"""Performs one discharge"""
		measure_interval = int(1000 * float(self.settings['discharge/measure_interval']))
		wait_after = int(self.settings['discharge/wait_after']) * 60
		voltage_limit = float(self.settings['discharge/voltage_limit'])

		self.create_new_cycle()
		self.change_stage(CYCLE_INITWAIT)
		self.wait_for(int(self.settings['measure/wait_before']) * 60)
			
		with self.discharger:
			self.change_stage(CYCLE_DISCHARGE)

			while True:
				data = self.charger.explicit_measure()
				voltage, voltage2, current, temp, status = data
				current = -current
				self.store_measure(voltage, voltage2, current, temp)
				logging.info(repr(data))
				if voltage < voltage_limit: break
				QThread.msleep(measure_interval)
				if self.skip_requested: break

		#progressbar = QProgressDialog("waiting", "cancel", 0, wait)
		#progressbar.setValue(0)
		self.change_stage(CYCLE_DISCHARGE_WAIT)
		self.wait_for(wait_after)
		
	def charge(self):
		measure_interval = int(1000 * float(self.settings['discharge/measure_interval']))
		wait_after = int(self.settings['charge/wait_after']) * 60
		temp_limit = int(self.settings['charge/temp_limit'])
		voltage_limit = float(self.settings['charge/voltage_limit'])
		error_temp = int(self.settings['charge/error_temp'])

		OK, OVERHEAT, SKIP = range(3)

		def temp_watch():
			self.change_stage(CYCLE_TEMPWATCH)
			while True:
				data = self.charger.explicit_measure()
				voltage, voltage2, current, temp, status = data
				self.store_measure(voltage, voltage2, current, temp)
				logging.info(repr(data))
				QThread.msleep(measure_interval)
				if temp <= temp_limit: return

		def try_charge():
			with self.charger:
				self.charge_start = time.time()
				self.change_stage(CYCLE_CHARGE)
				
				while True:
					data = self.charger.measure()
					logging.info("Charging, got %r", data)
					voltage, voltage2, current, temp, status = data
					self.store_measure(voltage, voltage2, current, temp)
	
					if temp > error_temp:
						logging.warning("Akkumulátor túlmelegedett")
						return OVERHEAT
					if voltage > voltage_limit:
						return OK
					if self.skip_requested:
						return SKIP

		def wait_after_charge():
			self.change_stage(CYCLE_CHARGE_WAIT)
			self.wait_for(wait_after)
			self.change_stage(CYCLE_END)

		while True: # repeat until 
			temp_watch()
			res = try_charge()
			if res == OVERHEAT:
				continue
			elif res == SKIP:
				return
			elif res == OK:
				break
		wait_after_charge()
############## Mock classes for easy testing #####
class MockDischarger:
	def __enter__(self):
		self.voltage = 13
	def __exit__(self, exc_type, exc_value, traceback):
		pass
	def measure(self):
		self.voltage -= 0.1
		return (self.voltage, 20)
	
class MockCharger:
	voltage = 11.7
	def __enter__(self):
		self.voltage = 11
	def __exit__(self, exc_type, exc_value, traceback):
		pass
	def explicit_measure(self):
		return self.measure()
	def measure(self):
		QThread.sleep(1)
		self.voltage += 0.1
		return (self.voltage, self.voltage - 0.5, 20, 24, 4)
